同一組 15 題,Day 11 的 Chroma 量到 top-1 命中 10/15。今天把它接上 LLM,
答案正確 15/15。
兩個數字不只不一樣,方向還是反的——我六天來當成成績單的那個指標,
天花板比實際表現還低 5 題。
(基準線取 Chroma 那一套;Day 12、13 換庫量到的 top-1 各有出入,
今天刻意繞回 Day 11。)
RAG=先從自己的資料撈出相關段落,再交給 LLM 寫答案(Day 8 有完整說明)。
前六天我只做了上半截:切塊(Day 9)、建索引(Day 10)、Chroma(Day 11),
Day 12、13 又換了,比較過一輪。成績單一直是同一個指標:top-1 命中率,Chroma 的數字是 10/15。
但這個指標藏著一個假設:看檢索結果的是人。人只想看第一名,所以第一名
要對。今天讀者換成 LLM——它一次讀完 k 段。假設一換,尺就得換。
今天刻意繞回 Chroma 而不用最後一天那套:要量生成端,檢索側就必須是已經有
基準線的那一套,落差才能乾淨歸因。語料與題目一字未改,檢索側參數全部凍結。
四步,全部在 pipeline.answer() 裡:
問題「特休沒休完會怎樣?」
├─(1) 算問題的向量 ← Day 8 做過
├─(2) Chroma 取 top-k 條文 ← Day 11 做過
├─(3) top-k 編號後組進 prompt ← 今天新增
└─(4) 丟給 LLM 產生答案 ← 今天新增
prompt 刻意只放兩條約束:
SYSTEM = (
"你是公司人資的規章問答助理。"
"請只依據使用者提供的「規章條文」回答,不要引用條文以外的法律知識或常識。"
"答案用繁體中文,控制在三句話以內,直接講結論與關鍵數字。"
)
第一句是 RAG 的命脈。少了它,模型會拿預訓練的勞基法常識作答——語料換掉
答案也不會變,那你架的不是 RAG,是一台會背法條的機器。(所以有一組
「不給任何條文」的對照組專門量這件事。)
第二句是為了錢:輸出 token 比輸入貴 4 倍。實測有效——對照組平均 63.9 字,
給了條文的三組是 39~43 字。
context 每段都編號([1] 第 23 條(第五章 請假管理) 這種格式)。不是為了
好看:今天要分析「答案排第幾名時模型還讀不讀得到」,明天要問它「你用了
第幾段」,都需要這個位置標記。
四組對照:無 context、k=1、k=3、k=5。三個指標一起看:
| 指標 | 定義 | 誰的成績 |
|---|---|---|
| top-1 命中率 | 預期條文排第一名 | Day 8~13 的舊尺 |
| recall@k | 預期條文出現在 k 個之內 | 檢索真正交給生成的 |
| 答案正確率 | 答案含有全部關鍵事實 | 今天的新尺 |
答案怎麼判對錯:我沒用 LLM 當裁判(裁判自己會錯、會偏心,重跑要再付錢,
數字不可重現),改用關鍵事實比對——每題預先寫下答案必須出現的事實,
全出現才算對。
{"id": 1, "question": "病假連續請幾天以上需要附診斷證明?",
"expected": "第 24 條", "facts": ["三日", "診斷證明"], "forbid": []}
比對前兩邊都正規化(中文數字轉阿拉伯數字、「天」=「日」、去標點),
所以模型答「連續請病假 3 天以上須附診斷證明書」也算對。
這把尺的偏誤先自首三個:
forbid 補這個洞,但只補得到facts 是我挑的,挑寬鬆分數就虛高。所以一律照條文原文取,不照模型先記著第 1、2 點。今天最漂亮的那個滿分,就是被它們一起放過去的。
| 組別 | top-1 | recall@k | 答案正確 | 答案字數 | context 字數 | tokens(輸入/輸出) |
|---|---|---|---|---|---|---|
| 無 context | — | — | 2/15 | 63.9 | 0.0 | 952/856 |
| k=1 | 10/15 | 10/15 | 10/15 | 39.3 | 137.5 | 3649/554 |
| k=3 | 10/15 | 14/15 | 14/15 | 42.0 | 412.1 | 7952/609 |
| k=5 | 10/15 | 14/15 | 15/15 | 42.8 | 696.0 | 12403/613 |
top-1 是同一次檢索算出來的,三組必然相同。該看的是中間兩欄走勢完全一致,
而最左邊那欄一動也沒動。
不給條文,15 題只對 2 題——病假三日附診斷證明、加班上限 46 小時,
都是規章剛好與勞基法一致的題。它答對的不是我的規章,是預訓練學過的
法律常識。
真正的證據在錯的 13 題:它不是拒答,是掛著「根據公司規章」講錯數字。
| 題目 | 對照組的答案 | 規章實際規定 |
|---|---|---|
| 婚假幾天? | 「根據公司規章……五天」 | 八日(第 27 條) |
| 事假一年最多? | 「根據公司規章……七天」 | 十四日(第 25 條) |
| 幾天以上要職代? | 「請假超過三天以上」 | 一日以上(第 30 條) |
| 特休沒休完? | 「自動作廢,無法累積」 | 遞延至次年 3/31,屆期折算工資(第 23 條) |
| 離職提前多久講? | 「至少兩週」 | 十日/二十日/三十日(第 54 條) |
「根據公司規章」這五個字最危險。它沒讀過我的規章,卻用這個句式把常識
包裝成內部規定——要是我只隨手問幾題、不逐題比對 facts,這些答案全會
通過我的目視檢查。
從 2/15 到 15/15,中間那 13 題全是檢索換來的。
| 組別 | 撈到且答對 | 撈到但答錯 | 沒撈到卻答對 | 沒撈到且答錯 |
|---|---|---|---|---|
| k=1 | 10 | 0 | 0 | 5 |
| k=3 | 14 | 0 | 0 | 1 |
| k=5 | 14 | 0 | 1 | 0 |
「檢索做對了、生成搞砸了」那一欄,三組都是 0。
我原本預期這裡會有題目——條文就在 prompt 裡,把兩條規定混在一起或抓錯
數字都很常見;今天一題都沒發生。只要條文進了 prompt,gpt-4o-mini 就把
數字照抄出來。
所以在這個規模下,答對率的天花板是 recall@k,不是 top-1。兩者在 k=3
差了 4 題,我前六天優化的是低的那個。
邊界要說清楚:語料 59 條、每題單一條文可答、答案都是數字與制度名詞。
多條文綜合題(「加班 3 小時領多少錢」要第 19 條+第 33 條)今天量不到。
| 預期條文排名 | k=3 題數/答對 | k=5 題數/答對 |
|---|---|---|
| 第 1 名 | 10/10 | 10/10 |
| 第 2 名 | 2/2 | 2/2 |
| 第 3 名 | 2/2 | 2/2 |
| 未進 top-k | 1/0 | 1/1 |
沒有「餵進去卻讀不到」的現象。兩個直接後果:該追 recall@k,不是 top-1
(排序好看是給人的需求,模型只在乎有沒有在裡面);先別急著上 reranker
——重排序解的是「排後面讀不到」,而這個問題今天不存在,k 拉大就解決了。
第 7 題「特休沒休完會怎樣?」:k=1 沒撈到第 23 條,模型老實回「規章條文
中未提及」,判定算錯;k=3 之後它排到第 3 名,答案就變成「得遞延至次年
三月三十一日止,屆期按日折算工資發給」,關鍵事實全中。同一個模型、
同一個 prompt,差別只在那段條文有沒有進 context。
k=5 唯一從「未進 top-k」逆轉的是第 9 題「沒來上班也沒跟任何人講,會有
什麼後果?」,預期第 32 條(曠職),facts 只要求出現「曠職」。
k=5 撈回第 47、12、49、13、16 條——第 32 條依然沒進 prompt。模型答:
未經請假或通知擅自缺勤,將視為曠職,依第八章獎懲規定處理。
判定 ✅。但「曠職」在整段 context 只出現一次,在第 16 條(天然災害停班):
……員工因災害實際受阻無法出勤者,得依第五章規定請假,
公司不得視為曠職。
語意相反。模型從「不得視為曠職」生成「將視為曠職」,結論恰好正確;
而第 32 條真正的規定(曠職不發工資、連續三日或一個月累計六日得終止契約)
從頭到尾沒進 prompt。
這題把前面自首的偏誤 1 和 2 同時演了一次:尺只查關鍵詞,而我把這題的facts 挑成單獨一個詞。 總表最誠實的寫法是「k=5:14/15 + 1 題蒙對」。
我留著不修,因為修掉就看不到這件事。要補有兩個方向:facts 加上
「不發工資」把門檻拉高,或像明天那樣改判引用的條號對不對——第 9 題在
k=1/k=3 都引了第 47 條,那是憑空安上去的依據,比答錯數字更難發現。
| 情境 | 建議 | 代價 |
|---|---|---|
| 評估自己的 RAG | 別只量檢索命中率,要量答案正確率——今天差 5 題 | 得先定義「答案對不對」,沒有免費解 |
| 挑指標 | 追 recall@k,別追 top-1 | 前提是生成端讀得到後面的段落,要自己驗 |
| 定判定標準 | 關鍵事實比對優先於 LLM 裁判,但 facts 別只寫一個詞 |
只適用答案含明確事實的語料 |
| 挑 k 值 | k=3 是甜蜜點:14/15,輸入 token 只有 k=5 的 64% | 輸入 token 隨 k 線性成長(k=1→5 是 3.4 倍) |
| 換真實語料前 | 條文原文會整段送進生成 API | 這是 Day 8~13 沒有的第二條外流路徑 |
順便注意對照組輸出 856 tokens 比 k=5 的 613 還多:沒有條文可講的時候,
它話最多。chat_cache.json 以「模型+完整 messages」雜湊為 key,temperature=0 下重跑走快取、拿到跟本文一樣的數字;刪掉兩個快取檔
重跑可重現完整成本。
檢索的成績單和 RAG 的成績單不是同一張。前六天我每天在把 top-1 從 9 推到
10,而真正決定答案對不對的是 recall@k——同一次檢索、同一份數據,我看的
是低的那一欄。
還有一個更難受的:今天生成端一題都沒搞砸,意思是這 15 題的難度根本不在
生成。全對的實驗代表考題太簡單,所以明天要加難度。
你的 RAG 上線前,量的是檢索命中率,還是答案正確率?如果是前者,你怎麼
知道模型真的讀懂了撈回來的東西?
明天 Day 15:今天有一類題目讓判定尺當機——模型說「條文未提及」的時候
(第 7 題 k=1 就是,答得老實卻算它錯)。反過來更麻煩:規章真的沒寫的
問題,它照樣掰得出答案,就像今天對照組那 13 題。明天加五題語料裡根本
沒有答案的陷阱題,量幻覺率、該拒答時拒答、過度拒答與引用正確率,
並要求它標出條號,看這會不會反而讓正常題答得更差。
GitHub:https://github.com/wp900622/TrustRAG (day14_rag/,clone 後python build_db.py && python run_experiment.py 即可重現)